﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using GenY.TypeWrapper.Internal;

namespace GenY.TypeWrapper {
  public class TypeWrapperGenYratorHost : MarshalByRefObject {
    [ImportMany]
    public Lazy<ITypeWrapper, ITypeWrapperMetadata>[ ] TypeWrappers { get; set; }

    public TypeWrapperGenYratorHost( ) {
      var container = new CompositionContainer( new DirectoryCatalog( Path.Combine( Path.GetDirectoryName( this.GetType( ).Assembly.Location ), "TypeWrappers" ) ) );
      container.ComposeParts( this );
    }

    public GenyResult GenYrate( string filename, string file, string language, string[ ] references ) {
      var element = XElement.Parse( file );
      var refs = references.Select( r => r.TryLoadAssembly( ) ).Where( r => r != null ).ToArray( );
      AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ReflectionOnlyAssemblyResolve;

      try {
        var x = from e in element.Elements( )
                let tw = TypeWrappers.FirstOrDefault( tw => tw.Metadata.Name == e.Name )
                where tw != null
                let includeNamespaces = e.ElementOrEmpty( "Include" ).Elements( "Namespace" ).Select( n => n.Attribute( "Name" ).Value ).ToHash( )
                let includeTypes = e.ElementOrEmpty( "Include" ).Elements( "Type" ).Select( n => new {
                  Name = n.Attribute( "Name" ).Value,
                  IncludedMembers = n.Elements( "IncludeMember" ).Select( im => im.Attribute( "Name" ).Value ).ToHash( ),
                  ExcludedMembers = n.Elements( "ExcludeMember" ).Select( im => im.Attribute( "Name" ).Value ).ToHash( )
                } ).ToArray( )
                let excludeNamespaces = e.ElementOrEmpty( "Exclude" ).Elements( "NameSpace" ).Select( n => n.Attribute( "Name" ).Value ).ToHash( )
                let excludeTypes = e.ElementOrEmpty( "Exclude" ).Elements( "Type" ).Select( n => n.Attribute( "Name" ).Value ).ToHash( )
                let types = from t in refs.SelectMany( a => a.GetTypes( ) )
                            let ns = t.Namespace != null ? t.Namespace : "Global::" //correct?
                            let includedType = includeTypes.FirstOrDefault( ti => t.FullName == ti.Name )
                            let isNotIncluded = includedType == null
                            where !excludeNamespaces.Contains( ns ) &&
                                  !excludeTypes.Contains( t.FullName ) &&
                                   ( includeNamespaces.Contains( ns ) || !isNotIncluded ) //double negation.. i know..
                            let filteredMembers = from m in t.GetMembers( BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly )
                                                  where isNotIncluded ||
                                                    ( ( !includedType.IncludedMembers.Any( ) || includedType.IncludedMembers.Any( n => m.Name == n ) ) && !includedType.ExcludedMembers.Any( n => m.Name == n ) )
                                                  select m
                            select new { Type = t, Members = filteredMembers }
                let generated = tw.Value.Wrap( types.ToDictionary( t => t.Type, t => t.Members ), e )
                select new GenyRatedFile { FileName = e.Name + ".cs", Contents = Encoding.ASCII.GetBytes( generated) };

        var res = new GenyResult { Report = "", Files = x.ToArray( ) };
        return res;
      }
      finally {
        AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomain_ReflectionOnlyAssemblyResolve;
      }
    }

    Assembly CurrentDomain_ReflectionOnlyAssemblyResolve( object sender, ResolveEventArgs args ) {
      return Assembly.ReflectionOnlyLoad( args.Name );
    }
  }

  static class TypeGenHelpers {
    public static XElement ElementOrEmpty( this XElement element, String name ) {
      var res = element.Element( name );
      if( res != null )
        return res;
      else
        return new XElement( name );
    }


    public static Assembly TryLoadAssembly( this string input ) {
      try {
        return Assembly.ReflectionOnlyLoad( AssemblyName.GetAssemblyName( input ).FullName );
      }
      catch( Exception e ) {
        try {
          return Assembly.ReflectionOnlyLoadFrom( input );
        }
        catch {
          return null;
        }
      }
    }

    public static HashSet<T> ToHash<T>( this IEnumerable<T> input ) {
      HashSet<T> res = new HashSet<T>( );
      foreach( var item in input )
        res.Add( item );
      return res;
    }

    public static string ToStringWithSeparator<T>( this IEnumerable<T> input, string separator ) {
      StringBuilder sb = new StringBuilder( );
      foreach( var item in input ) {
        sb.Append( item );
        sb.Append( separator );
      }

      if( sb.Length != 0 )
        sb.Remove( sb.Length - separator.Length, separator.Length );
      return sb.ToString( );
    }
  }

  [GenYrator( "tw" )]
  public class TypeWrapperGenYrator : IGenYrator {
    public GenyResult GenYrate( string filename, string file, string language, string[ ] references ) {
      var hosttype = typeof( TypeWrapperGenYratorHost );
      AppDomainSetup setup = new AppDomainSetup {
        ApplicationBase = Path.GetDirectoryName( typeof( IGenYrator ).Assembly.Location ),
        PrivateBinPath = "GenYRators;GenYRators\\TypeWrappers"
      };

      AppDomain d = AppDomain.CreateDomain( "gurka", null, setup );
      AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
      try {
        //invalidcast problem seems to have something to do with loadcontexts.
        // one solution is here: http://www.west-wind.com/WebLog/posts/601200.aspx
        //wonder if a separare load will do the trick, it didnt
        var host = ( TypeWrapperGenYratorHost )d.CreateInstanceAndUnwrap( hosttype.Assembly.FullName, hosttype.FullName );
        var res = host.GenYrate( filename, file, language, references );
        AppDomain.Unload( d );
        return res;
      }
      finally {
        AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve;
      }
    }

    Assembly AssemblyResolve( object sender, ResolveEventArgs args ) {
      Assembly res = null;
      AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve; //avoid a potential stack overflow if the assembly cant be found.
      try {
        res = Assembly.Load( args.Name );
      }
      finally {
        AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
      }
      return res;
    }
  }
}